﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;

namespace Nature.Data
{
    /// <summary>
    /// Provides a base class for a Thermo7 builder.
    /// </summary>
    public abstract class Thermo7RangeBuilder
    {
        /// <summary>
        /// Converts the given functional decomposition object into a new instance of the Thermo7 type
        /// </summary>
        /// <param name="decomposition">Functional decomposition object</param>
        /// <returns>New instance of the Thermo7 type</returns>
        internal protected abstract Thermo7Range CreateNewItem(FuncBasisDecomposition decomposition);

        #region Private Fields
        List<Thermo7Range> m_list = new List<Thermo7Range>(10);
        Dictionary<double, double> m_valuesLookup = new Dictionary<double, double>();
        Func<double, double>[] m_basis;
        IFuncBasisResolver m_resolver;
        #endregion

        #region Protected Members

        /// <summary>
        /// Initializes a new instance of the Thermo7Builder class.
        /// </summary>
        /// <param name="basis">Functional basis</param>
        /// <param name="resolver">Functional basis resolver</param>
        internal protected Thermo7RangeBuilder(Func<double, double>[] basis, IFuncBasisResolver resolver)
        {
            m_basis = basis;
            m_resolver = resolver ?? new Nature.Numerics.AHMms2004FuncBasisResolver();
        }

        /// <summary>
        /// Functional basis
        /// </summary>
        internal protected ReadOnlyArray<Func<double, double>> Basis { get { return m_basis; } }

        #endregion

        /// <summary>
        /// Array of built Thermo7 objects
        /// </summary>
        public Thermo7Range[] Ranges
        {
            get { return m_list.ToArray(); }
        }

        /// <summary>
        /// Absolute deviation of the computed fit from the input data
        /// </summary>
        public double AbsoluteDeviation { get; private set; }

        /// <summary>
        /// Relative deviation of the computed fit from the input data
        /// </summary>
        public double RelativeDeviation { get; private set; }

        /// <summary>
        /// Creates a new array of Thermo7 objects by approximating the provided values
        /// </summary>
        /// <param name="temperatureValues">Discrete set of temperature values [k]</param>
        /// <param name="valuesLookup">Lookup function providing the values of a thermodynamic potential (like e.g. Cp, H or S) 
        /// for the given value of temperature [K]</param>
        /// <returns></returns>
        public Thermo7Range[] Build(IEnumerable<double> temperatureValues, Func<double, double> valuesLookup)
        {
            m_list.Clear();
            AbsoluteDeviation = RelativeDeviation = 0.0;
            m_valuesLookup =
                (
                    from t in temperatureValues.Distinct()
                    select new
                    {
                        T = t,
                        Value = valuesLookup(t)
                    }
                ).ToDictionary(i => i.T, i => i.Value);
            Resolve(0.0F, 200.0F);
            Resolve(200.0F, 1000.0F);
            Resolve(1000.0F, 3500.0F);
            Resolve(3500.0F, 6000.0F);
            Resolve(6000.0F, 10000.0F);
            return this.Ranges;
        }


        /// <summary>
        /// Builds and enlists a new instance of Thermo7 type that contains a fit covering 
        /// temperatures in the specified range
        /// </summary>
        /// <param name="tmin">Lowest temperature [K]</param>
        /// <param name="tmax">Highers temperature [K]</param>
        private void Resolve(double tmin, double tmax)
        {
            tmin = Math.Max(tmin, m_valuesLookup.Keys.Min());
            tmax = Math.Min(tmax, m_valuesLookup.Keys.Max());
            if (tmin >= tmax) { return; }
            var range = ToRange(tmin, tmax);
            if (range == null) { return; }

            var decomposition = m_resolver.Decompose(
                    (t) => range[t],
                    range.Keys.OrderBy(t => t).ToArray(),
                    Basis
                );
            foreach (double temperature in m_valuesLookup.Keys.Where(t => tmin <= t && t <= tmax))
            {
                double value = m_valuesLookup[temperature];
                double approximation = decomposition.Calculate(temperature);
                double absDif = Math.Abs(value - approximation);
                double relDif = absDif / Math.Abs(value);
                AbsoluteDeviation = Math.Max(AbsoluteDeviation, absDif);
                RelativeDeviation = Math.Max(RelativeDeviation, relDif);
            }
            Thermo7Range th7 = CreateNewItem(decomposition);
            th7.Tmin = Math.Floor(tmin);
            th7.Tmax = Math.Floor(tmax);
            m_list.Add(th7);
        }

        /// <summary>
        /// Extracts the key - value pairs from the specified temperature range
        /// </summary>
        /// <param name="tmin">First temperature [K]</param>
        /// <param name="tmax">Second temperature [K]</param>
        /// <returns>Temperature vs. thermodynamic property lookup collection</returns>
        private Dictionary<double, double> ToRange(double tmin, double tmax)
        {
            if (m_valuesLookup.Count == 1)
            {
                double value = m_valuesLookup.Single().Value;
                return
                    (from i in Enumerable.Range(0, 5)
                     let t = tmin + i * (tmax - tmin) / 4d
                     select new { Temperature = t, Value = value })
                    .ToDictionary(i => i.Temperature, i => i.Value);
            }
            else if (m_valuesLookup.Count == 2)
            {
                var pairs = m_valuesLookup.OrderBy(i=> i.Value).ToArray();
                return
                    (from i in Enumerable.Range(0, 5)
                     let t = tmin + i * (tmax - tmin) / 4d
                     let v = pairs[0].Value + (pairs[1].Value - pairs[0].Value) / (pairs[1].Key - pairs[0].Key) * (t - pairs[0].Key)
                     select new { Temperature = t, Value = v })
                    .ToDictionary(i => i.Temperature, i => i.Value);
            }
            double[] temperatures = m_valuesLookup.Keys.Where(t => tmin <= t && t <= tmax).OrderBy(t => t).ToArray();
            if (temperatures.Length < 1) { return null; }
            var range =
                (
                        from t in temperatures
                        select new { T = t, Value = m_valuesLookup[t] }
                ).ToDictionary(i => i.T, i => i.Value);

            while (temperatures.Length < m_basis.Length)
            {
                for (int i = 1; i < temperatures.Length; ++i)
                {
                    double t = 0.5 * (temperatures[i] + temperatures[i - 1]);
                    double value = 0.5 * (range[temperatures[i]] + range[temperatures[i - 1]]);
                    range.Add(t, value);
                }
                temperatures = range.Keys.OrderBy(t => t).ToArray();
            }
            return range;
        }
    }


    /// <summary>
    /// Thermo7 builder computing the polynomial fit for the given values of the reduced heat capacity (Cp [~])
    /// </summary>
    [Export]
    public class CpThermo7Builder : Thermo7RangeBuilder
    {
        /// <summary>
        /// Initializes a new instance of the CpThermo7Builder class.
        /// </summary>
        /// <param name="resolver">Functional basis resolver</param>
        [ImportingConstructor]
        public CpThermo7Builder(
            [Import(typeof(IFuncBasisResolver), AllowDefault = true)]
            IFuncBasisResolver resolver = null)
            : base(
                new Func<double, double>[] 
                {
                    (double x)=> 1,
                    (double x)=> x,
                    (double x)=> x * x,
                    (double x)=> x * x * x,
                    (double x)=> x * x * x * x
                },
                resolver
            )
        {

        }

        /// <summary>
        /// Converts the given functional decomposition object into a new instance of the Thermo7 type
        /// </summary>
        /// <param name="decomposition">Functional decomposition object</param>
        /// <returns>New instance of the Thermo7 type</returns>
        protected internal override Thermo7Range CreateNewItem(FuncBasisDecomposition decomposition)
        {
            return new Thermo7Range
            {
                A1 = decomposition.Coefficients[0],
                A2 = decomposition.Coefficients[1],
                A3 = decomposition.Coefficients[2],
                A4 = decomposition.Coefficients[3],
                A5 = decomposition.Coefficients[4]
            };
        }
    }

    /// <summary>
    /// Thermo7 builder computing the polynomial fit for the given values of the reduced enthalpy (H [~])
    /// </summary>
    [Export]
    public class HThermo7Builder : Thermo7RangeBuilder
    {
        /// <summary>
        /// Initializes a new instance of the HThermo7Builder class.
        /// </summary>
        /// <param name="resolver">Functional basis resolver</param>
        [ImportingConstructor]
        public HThermo7Builder(
            [Import(typeof(IFuncBasisResolver), AllowDefault = true)]
            IFuncBasisResolver resolver = null)
            : base(
                new Func<double, double>[] 
                {
                    (double x)=> 1,
                    (double x)=> x / 2.0F,
                    (double x)=> x * x / 3.0F,
                    (double x)=> x * x * x / 4.0F,
                    (double x)=> x * x * x * x / 5.0F,
                    (double x)=> 1.0 / x
                },
                resolver
            )
        {

        }

        /// <summary>
        /// Converts the given functional decomposition object into a new instance of the Thermo7 type
        /// </summary>
        /// <param name="decomposition">Functional decomposition object</param>
        /// <returns>New instance of the Thermo7 type</returns>
        protected internal override Thermo7Range CreateNewItem(FuncBasisDecomposition decomposition)
        {
            return new Thermo7Range
            {
                A1 = decomposition.Coefficients[0],
                A2 = decomposition.Coefficients[1],
                A3 = decomposition.Coefficients[2],
                A4 = decomposition.Coefficients[3],
                A5 = decomposition.Coefficients[4],
                A6 = decomposition.Coefficients[5]
            };
        }
    }

    /// <summary>
    /// Thermo7 builder computing the polynomial fit for the given values of the reduced entropy (S [~])
    /// </summary>
    [Export]
    public class SThermo7Builder : Thermo7RangeBuilder
    {
        /// <summary>
        /// Initializes a new instance of the SThermo7Builder class.
        /// </summary>
        /// <param name="resolver">Functional basis resolver</param>
        [ImportingConstructor]
        public SThermo7Builder(
            [Import(typeof(IFuncBasisResolver), AllowDefault = true)]
            IFuncBasisResolver resolver = null)
            : base(
                new Func<double, double>[] 
                {
                    (double x)=> Math.Log(x),
                    (double x)=> x,
                    (double x)=> x * x / 2.0F,
                    (double x)=> x * x * x / 3.0F,
                    (double x)=> x * x * x * x / 4.0F,
                    (double x)=> 1.0
                },
                resolver
            )
        {

        }

        /// <summary>
        /// Converts the given functional decomposition object into a new instance of the Thermo7 type
        /// </summary>
        /// <param name="decomposition">Functional decomposition object</param>
        /// <returns>New instance of the Thermo7 type</returns>
        protected internal override Thermo7Range CreateNewItem(FuncBasisDecomposition decomposition)
        {
            return new Thermo7Range
            {
                A1 = decomposition.Coefficients[0],
                A2 = decomposition.Coefficients[1],
                A3 = decomposition.Coefficients[2],
                A4 = decomposition.Coefficients[3],
                A5 = decomposition.Coefficients[4],
                A7 = decomposition.Coefficients[5]
            };
        }
    }
}
